"""Calculator parameter values optimizer."""
from dataclasses import dataclass, field
from itertools import product
from pprint import pprint
from typing import Dict, List, Tuple

from .calculator import Calculator
from .rule_parameters import RuleParameters


def frange(start: float, stop: float, step: float):
    """Build a floating point numbers range."""
    d = stop - start
    n = int(d / step)
    return [start + step * i for i in range(n + 1)]


@dataclass
class Optimizer(object):
    """Parameter values optimizer for specific data."""

    planned: List[float]
    actual: List[float]
    expected: List[int]
    learning_rate: float = 1.0
    reduce_factor: float = 0.5
    lower_parameter_ranges: Dict[str, Tuple[float, float]] = field(
        default_factory=lambda: {
            "a": (0, 1),
            "b": (0, 10),
            "k": (0, 1),
            "z0": (-2, -1),
            # "m": (2, 4),
        }
    )
    upper_parameter_ranges: Dict[str, Tuple[float, float]] = field(
        default_factory=lambda: {
            "a": (0, 1),
            "b": (0, 10),
            "k": (0, 1),
            "z0": (1, 2),
            # "m": (2, 4),
        }
    )

    def __post_init__(self):
        """Initialize `z` ranges."""
        self.lower_parameter_ranges["z0"] = (
            min([a - p for a, p in zip(self.actual, self.planned) if a < p] or [-2]),
            max([a - p for a, p in zip(self.actual, self.planned) if a < p] or [-1]),
        )
        self.upper_parameter_ranges["z0"] = (
            min([a - p for a, p in zip(self.actual, self.planned) if a > p] or [1]),
            max([a - p for a, p in zip(self.actual, self.planned) if a > p] or [2]),
        )

    def optimize(self):
        """Optimize one pass."""
        lower_ranges = {
            param: frange(*limits, self.learning_rate)
            for param, limits in self.lower_parameter_ranges.items()
        }
        upper_ranges = {
            param: frange(*limits, self.learning_rate)
            for param, limits in self.upper_parameter_ranges.items()
        }
        lower_keys = sorted(lower_ranges)
        upper_keys = sorted(upper_ranges)
        lower_values = [lower_ranges[k] for k in lower_keys]
        upper_values = [upper_ranges[k] for k in upper_keys]
        best_loss = 100000
        for lower_comb in product(*lower_values):
            lower_dict = dict(zip(lower_keys, lower_comb))
            if lower_dict["a"] == 0 or lower_dict["b"] == 0 or lower_dict["k"] == 0:
                continue
            for upper_comb in product(*upper_values):
                try:
                    lower_rule_parameters = RuleParameters(
                        **dict(zip(lower_keys, lower_comb))
                    )
                except ZeroDivisionError:
                    continue
                try:
                    upper_rule_parameters = RuleParameters(
                        **dict(zip(upper_keys, upper_comb))
                    )
                except ZeroDivisionError:
                    continue
                if (
                    upper_rule_parameters.a == 0
                    or upper_rule_parameters.b == 0
                    or upper_rule_parameters.k == 0
                ):
                    continue
                c = Calculator(
                    lower_rule_parameters=lower_rule_parameters,
                    upper_rule_parameters=upper_rule_parameters,
                )
                c.calculate(self.planned, self.actual, self.expected)
                if c.total_loss < best_loss:
                    best_calc = c
                    best_loss = c.total_loss
        return best_calc


if __name__ == "__main__":
    o = Optimizer(
        planned=[10, 12, 14, 16, 18, 20, 22, 24, 26],
        actual=[9, 10, 11, 12, 13, 14, 15, 16, 17],
        expected=[2, 2, 2, 2, 2, 1, 1, 1, 1],
    )
    c = o.optimize()
    pprint(c)
